#include "dfhack_shared.h"
#include "room.h"
#include "ai.h"
#include "plan.h"

#include "modules/Maps.h"

#include "df/abstract_building.h"
#include "df/abstract_building_contents.h"
#include "df/building.h"
#include "df/tile_occupancy.h"
#include "df/unit.h"
#include "df/world.h"
#include "df/world_site.h"

REQUIRE_GLOBAL(ui);
REQUIRE_GLOBAL(world);

#define BEGIN_ENUM BEGIN_IMPLEMENT_ENUM
#define ENUM_ITEM IMPLEMENT_ENUM_ITEM
#define END_ENUM END_IMPLEMENT_ENUM
ROOM_ENUMS
#undef BEGIN_ENUM
#undef ENUM_ITEM
#undef END_ENUM

room::room(room_type::type type, df::coord mins, df::coord maxs, std::string comment) :
    status(room_status::plan),
    type(type),
    corridor_type(),
    farm_type(),
    stockpile_type(),
    nobleroom_type(),
    outpost_type(),
    location_type(),
    cistern_type(),
    workshop_type(),
    furnace_type(),
    raw_type(""),
    comment(comment),
    min(mins),
    max(maxs),
    accesspath(),
    layout(),
    owner(-1),
    bld_id(-1),
    squad_id(-1),
    level(-1),
    noblesuite(-1),
    queue(0),
    workshop(nullptr),
    users(),
    channel_enable(),
    stock_disable(),
    stock_specific1(false),
    stock_specific2(false),
    has_users(0),
    furnished(false),
    queue_dig(false),
    temporary(false),
    outdoor(false),
    channeled(false),
    build_when_accessible(false),
    required_value(0),
    data1(-1),
    data2(-1)
{
    channel_enable.clear();
    if (min.x > max.x)
        std::swap(min.x, max.x);
    if (min.y > max.y)
        std::swap(min.y, max.y);
    if (min.z > max.z)
        std::swap(min.z, max.z);
}

room::room(corridor_type::type subtype, df::coord mins, df::coord maxs, std::string comment) :
    room::room(room_type::corridor, mins, maxs, comment)
{
    corridor_type = subtype;
}

room::room(farm_type::type subtype, df::coord mins, df::coord maxs, std::string comment) :
    room::room(room_type::farmplot, mins, maxs, comment)
{
    farm_type = subtype;
}

room::room(stockpile_type::type subtype, df::coord mins, df::coord maxs, std::string comment) :
    room::room(room_type::stockpile, mins, maxs, comment)
{
    stockpile_type = subtype;
}

room::room(nobleroom_type::type subtype, df::coord mins, df::coord maxs, std::string comment) :
    room::room(room_type::nobleroom, mins, maxs, comment)
{
    nobleroom_type = subtype;
}

room::room(outpost_type::type subtype, df::coord mins, df::coord maxs, std::string comment) :
    room::room(room_type::outpost, mins, maxs, comment)
{
    outpost_type = subtype;
}

room::room(location_type::type subtype, df::coord mins, df::coord maxs, std::string comment) :
    room::room(room_type::location, mins, maxs, comment)
{
    location_type = subtype;
}

room::room(cistern_type::type subtype, df::coord mins, df::coord maxs, std::string comment) :
    room::room(room_type::cistern, mins, maxs, comment)
{
    cistern_type = subtype;
}

room::room(df::workshop_type subtype, df::coord mins, df::coord maxs, std::string comment) :
    room::room(room_type::workshop, mins, maxs, comment)
{
    workshop_type = subtype;
}

room::room(df::furnace_type subtype, df::coord mins, df::coord maxs, std::string comment) :
    room::room(room_type::furnace, mins, maxs, comment)
{
    furnace_type = subtype;
}

room::~room()
{
    for (auto it = layout.begin(); it != layout.end(); it++)
    {
        delete *it;
    }
}

void room::dig(bool plan, bool channel)
{
    for (int16_t x = min.x; x <= max.x; x++)
    {
        for (int16_t y = min.y; y <= max.y; y++)
        {
            for (int16_t z = min.z; z <= max.z; z++)
            {
                df::coord t(x, y, z);
                df::tiletype *tt = Maps::getTileType(t);
                if (tt)
                {
                    if (ENUM_ATTR(tiletype, material, *tt) == tiletype_material::CONSTRUCTION)
                    {
                        continue;
                    }
                    df::tile_dig_designation dm = channel ? tile_dig_designation::Channel : dig_mode(t);
                    if (((dm == tile_dig_designation::DownStair || dm == tile_dig_designation::Channel) && ENUM_ATTR(tiletype, shape, *tt) != tiletype_shape::STAIR_DOWN && ENUM_ATTR(tiletype_shape, basic_shape, ENUM_ATTR(tiletype, shape, *tt)) != tiletype_shape_basic::Open) || ENUM_ATTR(tiletype, shape, *tt) == tiletype_shape::WALL)
                    {
                        AI::dig_tile(t, dm);
                        if (plan)
                        {
                            Maps::getTileOccupancy(t)->bits.dig_marked = 1;
                        }
                    }
                }
            }
        }
    }

    if (plan)
        return;

    for (auto it = layout.begin(); it != layout.end(); it++)
    {
        furniture *f = *it;
        df::coord t = min + f->pos;
        df::tiletype *tt = Maps::getTileType(t);
        if (tt)
        {
            if (ENUM_ATTR(tiletype, material, *tt) == tiletype_material::CONSTRUCTION)
                continue;

            if (f->dig != tile_dig_designation::Default)
            {
                if (ENUM_ATTR(tiletype_shape, basic_shape, ENUM_ATTR(tiletype, shape, *tt)) == tiletype_shape_basic::Wall || (f->dig == tile_dig_designation::Channel && ENUM_ATTR(tiletype_shape, basic_shape, ENUM_ATTR(tiletype, shape, *tt)) != tiletype_shape_basic::Open))
                {
                    AI::dig_tile(t, f->dig);
                }
            }
            else
            {
                df::tile_dig_designation dm = dig_mode(t);
                if ((dm == tile_dig_designation::DownStair && ENUM_ATTR(tiletype, shape, *tt) != tiletype_shape::STAIR_DOWN) || ENUM_ATTR(tiletype, shape, *tt) == tiletype_shape::WALL)
                {
                    AI::dig_tile(t, dm);
                }
            }
        }
    }
}

bool room::include(df::coord t) const
{
    return min.x <= t.x && max.x >= t.x && min.y <= t.y && max.y >= t.y && min.z <= t.z && max.z >= t.z;
}

bool room::safe_include(df::coord t) const
{
    if (min.x - 1 <= t.x && max.x + 1 >= t.x && min.y - 1 <= t.y && max.y + 1 >= t.y && min.z <= t.z && max.z >= t.z)
        return true;

    for (auto it = layout.begin(); it != layout.end(); it++)
    {
        furniture *f = *it;
        df::coord ft = min + f->pos;
        if (ft.x - 1 <= t.x && ft.x + 1 >= t.x && ft.y - 1 <= t.y && ft.y + 1 >= t.y && ft.z == t.z)
            return true;
    }

    return false;
}

df::tile_dig_designation room::dig_mode(df::coord t) const
{
    for (auto f : layout)
    {
        if (min + f->pos == t && f->dig != tile_dig_designation::Default)
        {
            return f->dig;
        }
    }

    if (type != room_type::corridor)
    {
        return tile_dig_designation::Default;
    }
    bool wantup = include(t + df::coord(0, 0, 1));
    bool wantdown = include(t - df::coord(0, 0, 1));

    // XXX
    extern std::unique_ptr<AI> dwarfAI;
    wantup = wantup || dwarfAI->plan.corridor_include_hack(this, t, t + df::coord(0, 0, 1));
    wantdown = wantdown || dwarfAI->plan.corridor_include_hack(this, t, t - df::coord(0, 0, 1));

    if (wantup)
        return wantdown ? tile_dig_designation::UpDownStair : tile_dig_designation::UpStair;
    else
        return wantdown ? tile_dig_designation::DownStair : tile_dig_designation::Default;
}

bool room::is_dug(std::ostream & reason, df::tiletype_shape_basic want) const
{
    std::set<df::coord> holes;
    for (auto f : layout)
    {
        if (f->ignore)
            continue;

        df::coord ft = min + f->pos;

        if (f->dig == tile_dig_designation::No)
        {
            holes.insert(ft);
            continue;
        }

        auto tt = *Maps::getTileType(ft);
        auto sb = ENUM_ATTR(tiletype_shape, basic_shape, ENUM_ATTR(tiletype, shape, tt));
        switch (sb)
        {
        case tiletype_shape_basic::Wall:
            if (f->dig == tile_dig_designation::Default)
            {
                reason << "interior tile at (" << f->pos.x << ", " << f->pos.y << ", " << f->pos.z << ") is " << enum_item_key(tt);
                return false;
            }
            reason << enum_item_key(f->dig) << "-designated tile at (" << f->pos.x << ", " << f->pos.y << ", " << f->pos.z << ") is " << enum_item_key(tt);
            return false;
        case tiletype_shape_basic::Open:
            break;
        default:
            if (f->dig == tile_dig_designation::Channel)
            {
                reason << "Channel-designated tile at (" << f->pos.x << ", " << f->pos.y << ", " << f->pos.z << ") is " << enum_item_key(tt);
                return false;
            }
            break;
        }
    }
    for (int16_t x = min.x; x <= max.x; x++)
    {
        for (int16_t y = min.y; y <= max.y; y++)
        {
            for (int16_t z = min.z; z <= max.z; z++)
            {
                if (holes.count(df::coord(x, y, z)))
                {
                    continue;
                }

                auto tt = *Maps::getTileType(x, y, z);
                df::tiletype_shape s = ENUM_ATTR(tiletype, shape, tt);
                if (s == tiletype_shape::WALL)
                {
                    reason << "interior tile at (" << (x - min.x) << ", " << (y - min.y) << ", " << (z - min.z) << ") is " << enum_item_key(tt);
                    return false;
                }
                if (want != tiletype_shape_basic::None)
                {
                    df::tiletype_shape_basic sb = ENUM_ATTR(tiletype_shape, basic_shape, s);
                    if (want != sb)
                    {
                        reason << "tile at (" << (x - min.x) << ", " << (y - min.y) << ", " << (z - min.z) << ") is " << enum_item_key(tt) << " (" << enum_item_key(sb) << ") but want " << enum_item_key(want);
                        return false;
                    }
                }
            }
        }
    }
    return true;
}

bool room::constructions_done(std::ostream & reason) const
{
    for (auto it = layout.begin(); it != layout.end(); it++)
    {
        furniture *f = *it;

        df::coord ft = min + f->pos;

        auto tt = *Maps::getTileType(ft);
        auto ts = ENUM_ATTR(tiletype, shape, tt);

        df::tiletype_shape want;
        switch (f->construction)
        {
        case construction_type::NONE:
            continue;
        case construction_type::Fortification:
            want = tiletype_shape::FORTIFICATION;
            break;
        case construction_type::Wall:
            want = tiletype_shape::WALL;
            break;
        case construction_type::Floor:
            want = tiletype_shape::FLOOR;
            break;
        case construction_type::UpStair:
            want = tiletype_shape::STAIR_UP;
            break;
        case construction_type::DownStair:
            want = tiletype_shape::STAIR_DOWN;
            break;
        case construction_type::UpDownStair:
            want = tiletype_shape::STAIR_UPDOWN;
            break;
        case construction_type::Ramp:
            want = tiletype_shape::RAMP;
            break;
        default:
            reason << "unknown construction type " << enum_item_key(f->construction) << " at (" << f->pos.x << ", " << f->pos.y << ", " << f->pos.z << ")";
            return false;
        }

        if (want == tiletype_shape::FLOOR)
        {
            if (ts == tiletype_shape::PEBBLES || ts == tiletype_shape::BOULDER || ts == tiletype_shape::BROOK_TOP)
                continue;

            if (ts == tiletype_shape::SAPLING && type == room_type::pasture)
                continue;

            // shrubs are allowed here but not in fixup_opena. this means that
            // shrubs don't prevent pastures from being built, but they also
            // don't stick around in pastures forever, potentially using up
            // all the space for grass.
            if (ts == tiletype_shape::SHRUB && type == room_type::pasture)
                continue;
        }

        if (ts != want)
        {
            reason << "want construction type " << enum_item_key(f->construction) << " at (" << f->pos.x << ", " << f->pos.y << ", " << f->pos.z << ") but have " << enum_item_key(tt) << " (" << enum_item_key(ts) << ") instead of " << enum_item_key(want);
            return false;
        }
    }
    return true;
}

df::building *room::dfbuilding() const
{
    return df::building::find(bld_id);
}

int32_t room::compute_value() const
{
    auto bld = dfbuilding();
    if (!bld)
    {
        return -1;
    }

    if (type == room_type::location && bld->location_id != -1)
    {
        auto loc = binsearch_in_vector(ui->main.fortress_site->buildings, bld->location_id);
        if (loc && loc->getContents())
        {
            return loc->getContents()->location_value;
        }
    }

    auto u = df::unit::find(owner);
    if (!u)
    {
        u = world->units.active[0];
    }

    return bld->getRoomValue(u);
}

int32_t room::distance_to(const room *other) const
{
    if (this == other)
    {
        return 0;
    }

    std::map<const room *, int32_t> path_distance;
    int32_t distance = 0;
    std::vector<room *> current_level = accesspath;
    std::vector<room *> next_level;

    path_distance[this] = 0;

    while (!current_level.empty())
    {
        distance++;

        for (room *r : current_level)
        {
            if (!path_distance.count(r))
            {
                if (r == other)
                {
                    return distance;
                }

                path_distance[r] = distance;
                next_level.insert(next_level.end(), r->accesspath.begin(), r->accesspath.end());
            }
        }

        current_level = std::move(next_level);
        next_level.clear();
    }

    std::set<const room *> seen;
    seen.insert(other);

    int32_t max_distance = distance;
    distance = 0;
    current_level = other->accesspath;

    while (!current_level.empty())
    {
        distance++;

        for (room *r : current_level)
        {
            if (!seen.count(r))
            {
                if (path_distance.count(r))
                {
                    return path_distance.at(r) + distance;
                }

                seen.insert(r);
                next_level.insert(next_level.end(), r->accesspath.begin(), r->accesspath.end());
            }
        }

        current_level = std::move(next_level);
        next_level.clear();
    }

    // not connected
    return 0x10000 + distance + max_distance;
}

bool room::low_grass() const
{
    df::coord sz = size();
    int32_t size_tiles = sz.x * sz.y;
    int32_t grass_tiles = 0;

    for (int16_t x = 0; x <= sz.x; x++)
    {
        for (int16_t y = 0; y < sz.y; y++)
        {
            if (auto tt = Maps::getTileType(min + df::coord(x, y, 0)))
            {
                auto tm = ENUM_ATTR(tiletype, material, *tt);
                if (tm == tiletype_material::GRASS_LIGHT || tm == tiletype_material::GRASS_DARK)
                {
                    grass_tiles++;
                }
            }
        }
    }

    return grass_tiles < size_tiles / 5;
}
